﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.ExtendedReflection.Metadata;
using Microsoft.ExtendedReflection.Utilities.Safe.Diagnostics;
using PexMe.Core;
using System.IO;
using System.Reflection.Emit;
using PexMe.Common;

namespace PexMe.PUTGenerator
{
    /// <summary>
    /// Generates a PUT and compiles the entire assembly for alternative methods
    /// that need to be explored
    /// </summary>
    public class PUTGenerator
    {        
        /// <summary>
        /// Returns a method call for a PUT. This should be of certain characteristics,
        /// It should contain only one method call as generated by us
        /// </summary>
        /// <param name="putmethod"></param>
        /// <returns></returns>
        public static bool TryRetrieveMethodCall(Method putmethod, out Method assocmethod)
        {
            SafeDebug.AssumeNotNull(putmethod, "putmethod");
            assocmethod = null;

            MethodBodyEx mbodyex;
            bool bresult = putmethod.TryGetBody(out mbodyex);
            SafeDebug.Assert(bresult, "Failed to get the body");

            int offset = 0;
            Instruction instruction;

            List<Method> allCalledMethods = new List<Method>();
            while (mbodyex.TryGetInstruction(offset, out instruction))
            {
                SafeDebug.AssumeNotNull(instruction, "instruction");
                OpCode opCode = instruction.OpCode;

                if (opCode == OpCodes.Call || opCode == OpCodes.Callvirt)
                {
                    SafeDebug.Assume(opCode.OperandType == OperandType.InlineMethod, 
                        "opCode.OperandType == OperandType.InlineMethod");
                    allCalledMethods.Add(instruction.Method);                    
                }

                offset = instruction.NextOffset;
            }

            if (allCalledMethods.Count != 1)
                return false;

            assocmethod = allCalledMethods[0];
            return true;
        }


        /// <summary>
        /// Retrieves the PUT for the method we are looking for in the current assembly
        /// </summary>
        /// <param name="method"></param>
        /// <returns></returns>
        public static bool TryRetrievePUT(AssemblyEx assembly, Method method, out Method putmethod)
        {
            //Get the class name of the target class that is including
            //the method we are looking for
            TypeEx declaringType;
            bool bresult = method.TryGetDeclaringType(out declaringType);
            SafeDebug.Assume(bresult, "Failed to get the declaring type!!!");

            var tclassname = declaringType.ShortName.ToString();
            tclassname = tclassname + "Test";

            foreach (var classdef in assembly.TypeDefinitions)
            {
                if(classdef.ShortName != tclassname)
                    continue;

                //TODO: To be more robust, we can later replace
                //this piece of code by actually checking whether there
                //is a method call to the method of the library we are looking for
                foreach (var mdef in classdef.DeclaredInstanceMethods)
                {
                    if (mdef.ShortName == method.ShortName)
                    {
                        bool isPexMethod = false;
                        foreach (var attr in mdef.DeclaredAttributes)
                        {
                            if (attr.SerializableName.ToString().Contains("PexMethodAttribute"))
                            {
                                isPexMethod = true;
                                break;
                            }
                        }

                        if (isPexMethod)
                        {
                            putmethod = mdef.Instantiate(MethodOrFieldAnalyzer.GetGenericTypeParameters(classdef), TypeEx.NoTypes);
                            return true;
                        }
                    }
                }
            }

            putmethod = null;
            return false;
        }

        public static string GeneratePUTCommand(Method putmethod)
        {
            TypeEx classdef;
            bool bresult = putmethod.TryGetDeclaringType(out classdef);
            SafeDebug.Assume(bresult, "Declaring type cannot be null");

            var assemblyEx = classdef.Definition.Module.Assembly;
            var assemblyName = assemblyEx.Location;
            var namespacestr = classdef.Namespace;

            StringBuilder sb = new StringBuilder();
            sb.Append("pex.exe ");
            sb.Append(assemblyName);
            sb.Append(" /nf:");
            sb.Append(namespacestr);
            sb.Append(" /tf:");
            sb.Append(classdef.ShortName.ToString());
            sb.Append(" /mf:");
            sb.Append(putmethod.ShortName.ToString());
            sb.Append(" /nor");
            sb.Append(" /rn:");
            sb.Append(classdef.ShortName.ToString() + "_" + putmethod.ShortName.ToString());
            if(PexMeConstants.ENABLE_DEBUGGING_MODE)
                sb.Append(" /bos");
            sb.Append(" /fullmscorlib");
            return sb.ToString();
        }
    }
}
